﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 
// See License.txt in the project root for license information.
using System.Text;
using Newtonsoft.Json;
using System.Reflection;
using System.IO;
using System.Threading.Tasks;
using System.Threading.Actors.Utilities;
using System.Diagnostics.Contracts;
using Newtonsoft.Json.Linq;

namespace System.Threading.Actors
{
    public static class SerializationHelper
    {
        private static JsonSerializerSettings s_noEmbeddedTypes = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.None };
        private static JsonSerializerSettings s_embeddedTypes = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All };
        public static Encoding PreferredEncoding
        {
            get { return Encoding.UTF8; }
        }
        //private const String s_hackPrefix = "HACK|";

        static SerializationHelper()
        {
#if !PLIB_HACK
            AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolver;
#endif
        }

        //
        // These first couple of methods were meant to be used to convey objects of known types,
        // so that we weren't embedding type information into our json strings.  I've backed them
        // out for now because we weren't using them and I suspected that calls to the other
        // methods were mistakenly being resolved to these.  
        //
        // We can add these back in later (probably renamed).
        //

        //public static string Serialize<T>(object obj)
        //{
        //    return JsonConvert.SerializeObject(obj, Formatting.None, s_noEmbeddedTypes);
        //}

        //public static T Deserialize<T>(string json)
        //{
        //    return JsonConvert.DeserializeObject<T>(json, s_noEmbeddedTypes);
        //}

        // Hack: JSON.NET doesn't support serializing BigIntegers.  For testing, we want to
        // support BigIntegers as well as a List<Object> holding at least one BigInteger.
        // This will not work with JavaScript, but shows us how to make .NET clients work with 
        // types that aren't currently well-supported by JSON.NET.
        // For progress on JSON.NET issue, check http://json.codeplex.com/workitem/23584
        public static string Serialize(object obj)
        {
            //if (obj is BigInteger)
            //    return s_hackPrefix + "BigInteger|" + ((BigInteger)obj).ToString(CultureInfo.InvariantCulture);
            //if (obj is Object[])
            //{
            //    Object[] array = (Object[])obj;
            //    StringBuilder sb = new StringBuilder();
            //    sb.Append(s_hackPrefix);
            //    sb.Append("Object[]|");
            //    sb.Append(array.Length);
            //    foreach (Object item in array) {
            //        sb.Append('|');
            //        String s = Serialize(item);
            //        // Write out the length-prefixed form so we avoid conflicts between delimiters and characters in our serialized output.
            //        sb.Append(s.Length);
            //        sb.Append(',');
            //        sb.Append(s);
            //    }
            //    return sb.ToString();
            //}
            //else if (obj is List<Object>)
            //{
            //    List<Object> list = (List<Object>)obj;
            //    StringBuilder sb = new StringBuilder();
            //    sb.Append(s_hackPrefix);
            //    sb.Append("List<Object>|");
            //    sb.Append(list.Count);
            //    foreach (Object item in list)
            //    {
            //        sb.Append('|');
            //        String s = Serialize(item);
            //        // Write out the length-prefixed form so we avoid conflicts between delimiters and characters in our serialized output.
            //        sb.Append(s.Length);
            //        sb.Append(',');
            //        sb.Append(s);
            //    }
            //    return sb.ToString();
            //}

            return JsonConvert.SerializeObject(obj, Formatting.None, s_noEmbeddedTypes);
        }

        public static object Deserialize(string json)
        {
            //if (json.StartsWith(s_hackPrefix))
            //{
            //    int index = json.IndexOf('|', s_hackPrefix.Length + 1);
            //    String typeName = json.Substring(s_hackPrefix.Length, index - s_hackPrefix.Length);
            //    if (typeName == "BigInteger")
            //        return BigInteger.Parse(json.Substring(index + 1), CultureInfo.InvariantCulture);
            //    else if (typeName == "Object[]" || typeName == "List<Object>")
            //    {
            //        int startLength = index + 1;
            //        int endLength = json.IndexOf('|', startLength + 1);
            //        String numElementsStr = json.Substring(startLength, endLength - startLength);
            //        int numElements = Int32.Parse(numElementsStr);
            //        Object[] array = new Object[numElements];
            //        int currentIndex = endLength + 1;
            //        for (int i = 0; i < numElements; i++) {
            //            int currentComma = json.IndexOf(',', currentIndex);
            //            int strLen = Int32.Parse(json.Substring(currentIndex, currentComma - currentIndex));
            //            array[i] = Deserialize(json.Substring(currentComma + 1, strLen));
            //            currentIndex = currentComma + 1 + strLen + 1;
            //        }
            //        return typeName == "Object[]" ? (Object) array : (Object) new List<Object>(array);
            //    }
            //    else
            //        throw new NotSupportedException(String.Format("Unsupported hacked serialization type: {0}", typeName));
            //}

            var result = JsonConvert.DeserializeObject(json, s_noEmbeddedTypes);

            if (result is JObject) result = new JsonObjectWrapper((JObject)result);
            else if (result is JArray) result = new JsonObjectWrapper((JArray)result);

            // KLUDGE: ints are deserialized as longs.  Try to turn them back into ints, if they'll fit.
            else if (result is long)
            {
                long r = (long)result;
                if (r >= Int32.MinValue && r <= Int32.MaxValue)
                    result = (int)r;
            }

            return result;
        }

        public static T DeserializeToType<T>(string json) { return JsonConvert.DeserializeObject<T>(json); }

#if !PLIB_HACK
        // The JSON serializer requires an assembly resolver so that we can deserialize already-loaded
        // assemblies where we attempt to load them by partial name (like "System.Numerics").
        private static Assembly AssemblyResolver(Object sender, ResolveEventArgs args)
        {
            foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())
                if (a.GetName().Name == args.Name)
                    return a;
            return null;
        }
#endif

        // TODO: Should we be looking for some kind of delimiter sequence when deserializing?
        // Maybe something like the 4-byte sequence "0xdeadd0d0"?  That way, if a reader
        // somehow loses its place, it can re-sync.

        /// <summary>
        /// Represents an actor service request being sent across the wire (like a method call).
        /// </summary>
        /// <param name="req">ActorServiceRequest to send</param>
        /// <param name="stream">Writable stream to send to.</param>
        public static void SerializeActorServiceRequest(ActorServiceRequest req, Stream stream)
        {
            Contract.Requires(req != null);
            Contract.Requires(stream != null);
            Contract.Requires(stream.CanWrite);

            var outString = req.ToJson();
            var outBytes = PreferredEncoding.GetBytes(outString);
            NetworkHelpers.WriteRequestPacket(stream, outBytes, outBytes.Length);
        }

        /// <summary>
        /// Represents an actor service request being sent across the wire (like a method call).
        /// </summary>
        /// <param name="req">ActorServiceRequest to send</param>
        /// <param name="stream">Writable stream to send to.</param>
        /// <param name="semaphore">For multiplexed streams being accessed by multiple threads, acquire this semaphore first before writing.</param>
        public static void SerializeActorServiceRequest(ActorServiceRequest req, Stream stream, SemaphoreSlim semaphore)
        {
            Contract.Requires(req != null);
            Contract.Requires(stream != null);
            Contract.Requires(stream.CanWrite);
            Contract.Requires(semaphore != null);

            var outString = req.ToJson();
            var outBytes = PreferredEncoding.GetBytes(outString);
            bool tookLock = false;
            try
            {
                semaphore.Wait();
                tookLock = true;
                NetworkHelpers.WriteRequestPacket(stream, outBytes, outBytes.Length);
            }
            finally
            {
                if (tookLock)
                    semaphore.Release();
            }
        }
        
        public static async Task<ActorServiceRequest> DeserializeActorServiceRequestAsync(
            Stream stream,
            byte[] messageBuffer)
        {
            Contract.Requires(stream != null);
            Contract.Requires(messageBuffer != null);

            var tuple = await NetworkHelpers.ReadRequestPacketAsync(stream, messageBuffer).ConfigureAwait(continueOnCapturedContext: false);
            var inString = PreferredEncoding.GetString(tuple.Item1, 0, tuple.Item2);
            return ActorServiceRequest.FromJson(inString);
        }

        public static void SerializeActorServiceResult(ActorServiceResult result, Stream stream, SemaphoreSlim semaphore = null)
        {
            Contract.Requires(result != null);
            Contract.Requires(stream != null);

            var outString = result.ToJson();
            SerializeActorServiceResult(outString, stream, semaphore);
        }

        public static void SerializeActorServiceResult(string asrString, Stream stream, SemaphoreSlim semaphore)
        {
            Contract.Requires(asrString != null);
            Contract.Requires(stream != null);

            var outBytes = PreferredEncoding.GetBytes(asrString);
            bool tookLock = false;
            try
            {
                if (semaphore != null)
                {
                    semaphore.Wait();
                    tookLock = true;
                }
                stream.WriteByte((byte)0); // Differentiate between service response and event publication
                NetworkHelpers.WriteRequestPacket(stream, outBytes, outBytes.Length);
            }
            finally
            {
                if (tookLock)
                    semaphore.Release();
            }
        }

        public static async Task SerializeActorServiceResultAsync(string asrString, Stream stream, SemaphoreSlim semaphore)
        {
            Contract.Requires(asrString != null);
            Contract.Requires(stream != null);

            var outBytes = PreferredEncoding.GetBytes(asrString);
            bool tookLock = false;
            try
            {
                if (semaphore != null)
                {
                    await semaphore.WaitAsync();
                    tookLock = true;
                }
                stream.WriteByte((byte)0); // Differentiate between service response and event publication
                await NetworkHelpers.WriteRequestPacketAsync(stream, outBytes, outBytes.Length);
            }
            finally
            {
                if (tookLock)
                    semaphore.Release();
            }
        }

        public static void SerializeActorEvent(ActorEvent ae, Stream stream, SemaphoreSlim semaphore = null)
        {
            Contract.Requires(ae != null);
            Contract.Requires(stream != null);

            var outString = ae.ToJson();
            var outBytes = PreferredEncoding.GetBytes(outString);
            bool tookLock = false;
            try
            {
                if (semaphore != null)
                {
                    semaphore.Wait();
                    tookLock = true;
                }
                stream.WriteByte((byte)1); // Differentiate between service response and event publication
                NetworkHelpers.WriteRequestPacket(stream, outBytes, outBytes.Length);
            }
            finally
            {
                if (tookLock)
                    semaphore.Release();
            }
        }

        public static async Task SerializeActorEventAsync(ActorEvent ae, Stream stream, SemaphoreSlim semaphore = null)
        {
            Contract.Requires(ae != null);
            Contract.Requires(stream != null);

            var outString = ae.ToJson();
            var outBytes = PreferredEncoding.GetBytes(outString);
            bool tookLock = false;
            try
            {
                if (semaphore != null)
                {
                    await semaphore.WaitAsync();
                    tookLock = true;
                }
                stream.WriteByte((byte)1); // Differentiate between service response and event publication
                await NetworkHelpers.WriteRequestPacketAsync(stream, outBytes, outBytes.Length);
            }
            finally
            {
                if (tookLock)
                    semaphore.Release();
            }
        }

        public static async Task<object> DeserializeActorFeedbackAsync(Stream stream, byte[] shortMessageBuf)
        {
            // Read in the feedback packet, which will be type discriminator (one byte), message length (four bytes)
            // and a byte-array message.
            var tuple = await NetworkHelpers.ReadFeedbackPacketAsync(stream, shortMessageBuf).ConfigureAwait(continueOnCapturedContext: false);
            var inBytes = tuple.Item1;
            var inCount = tuple.Item2;
            var rType = tuple.Item3;

            var inString = PreferredEncoding.GetString(inBytes, 0, inCount);
            if (rType == 0)
            {
                return ActorServiceResult.FromJson(inString);
                //Console.WriteLine("ASR inString: \"{0}\"", inString);
            }
            else if (rType == 1)
            {
                return ActorEvent.FromJson(inString);
                //Console.WriteLine("AE inString: \"{0}\"", inString);
            }
            else throw new InvalidOperationException("Unknown response type " + rType);
        }
    }
}
